// SPDX-License-Identifier: GPL-2.0-or-later
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/printk.h>

#include <scsi/scsi.h>
#include <scsi/scsi_cmnd.h>
#include <scsi/scsi_device.h>
#include <scsi/scsi_host.h>
#include <scsi/scsicam.h>
#include <scsi/scsi_eh.h>
#include <scsi/scsi_tcq.h>
#include <scsi/scsi_dbg.h>
#include <scsi/scsi_cmd_cache.h>

#include "scsi_logging.h"

#define SCSI_HANDLER_LIST_MAX_CNT 500

static atomic_t list_cnt = ATOMIC_INIT(0);
static LIST_HEAD(scsi_mgmt_handler_list);
static DEFINE_SPINLOCK(scsi_handler_list_lock);
static bool scsi_cmd_cache_enabled;
module_param(scsi_cmd_cache_enabled, bool, 0600);
MODULE_PARM_DESC(scsi_cmd_cache_enabled,
		"Turn on scsi command cache feature. Default is off.");

MODULE_LICENSE("GPL");

#define SCSI_CMD_CACHE_DBG(fmt, arg...)				\
	SCSI_LOG_SCAN_BUS(3,							\
		pr_info("scsi_cmd_cache %s " fmt,	\
				__func__, ##arg))

#define SCSI_CMD_CACHE_ERR(fmt, arg...)			\
	pr_err("scsi_cmd_cache %s " fmt,	\
			__func__, ##arg)

struct scsi_mgmt_handler {
	struct list_head list;
	unsigned int host_no;
	struct scsi_mgmt_template *template;
	struct kref kref;
};

static void scsi_mgmt_handler_release(struct kref *kref)
{
	struct scsi_mgmt_handler *handler =
		container_of(kref, struct scsi_mgmt_handler, kref);

	SCSI_CMD_CACHE_DBG("Freeing handler of %d\n", handler->host_no);

	kfree(handler);
}

static inline
struct scsi_mgmt_handler *scsi_mgmt_handler_get(struct scsi_mgmt_handler *handler)
{
	if (handler)
		kref_get(&handler->kref);

	return handler;
}

static inline void scsi_mgmt_handler_put(struct scsi_mgmt_handler *handler)
{
	if (handler)
		kref_put(&handler->kref, scsi_mgmt_handler_release);
}

static struct scsi_mgmt_handler *scsi_mgmt_handler_lookup(unsigned int host_no)
{
	struct scsi_mgmt_handler *tmp = NULL;
	struct scsi_mgmt_handler *found = NULL;

	spin_lock(&scsi_handler_list_lock);
	list_for_each_entry(tmp, &scsi_mgmt_handler_list, list) {
		if (tmp->host_no == host_no) {
			found = scsi_mgmt_handler_get(tmp);
			break;
		}
	}
	spin_unlock(&scsi_handler_list_lock);

	return found;
}

/**
 * scsi_mgmt_handler_register - register scsi command cache callbacks
 * @host_no: SCSI host number
 * @template: command cache callbacks
 *
 * Notes:
 *	Host must has already been added
 **/
int scsi_mgmt_handler_register(unsigned int host_no,
		struct scsi_mgmt_template *template)
{
	struct Scsi_Host *shost = NULL;
	struct scsi_mgmt_handler *handler = NULL;

	if (!template)
		return -EINVAL;

	shost = scsi_host_lookup(host_no);
	if (!shost) {
		SCSI_CMD_CACHE_ERR("Host%d did not add\n", host_no);
		return -EINVAL;
	}
	scsi_host_put(shost);

	handler = scsi_mgmt_handler_lookup(host_no);
	if (handler) {
		SCSI_CMD_CACHE_ERR("Callbacks for Host%d has already registered\n",
				host_no);
		scsi_mgmt_handler_put(handler);
		return -EINVAL;
	}

	if (atomic_read(&list_cnt) > SCSI_HANDLER_LIST_MAX_CNT)
		return -ENOMEM;

	handler = kzalloc(sizeof(struct scsi_mgmt_handler), GFP_KERNEL);
	if (!handler) {
		SCSI_CMD_CACHE_ERR("Failed to alloc handler\n");
		return -ENOMEM;
	}

	handler->host_no = host_no;
	handler->template = template;
	INIT_LIST_HEAD(&handler->list);
	kref_init(&handler->kref);

	spin_lock(&scsi_handler_list_lock);
	list_add_tail(&handler->list, &scsi_mgmt_handler_list);
	spin_unlock(&scsi_handler_list_lock);

	atomic_inc(&list_cnt);

	SCSI_CMD_CACHE_DBG("Register successful for Host%d", host_no);

	return 0;
}
EXPORT_SYMBOL(scsi_mgmt_handler_register);

/**
 * scsi_mgmt_handler_unregister - unregister scsi command cache callbacks
 * @host_no: SCSI host number
 *
 * Notes:
 *	Host must has already been added
 **/
void scsi_mgmt_handler_unregister(unsigned int host_no)
{
	struct scsi_mgmt_handler *tmp = NULL;
	int found = 0;

	spin_lock(&scsi_handler_list_lock);
	list_for_each_entry(tmp, &scsi_mgmt_handler_list, list) {
		if (tmp->host_no == host_no) {
			list_del(&tmp->list);
			atomic_dec(&list_cnt);
			found = 1;
			break;
		}
	}
	spin_unlock(&scsi_handler_list_lock);

	if (found)
		scsi_mgmt_handler_put(tmp);
	else
		SCSI_CMD_CACHE_ERR("Callbacks for Host%d has not registered\n",
				host_no);
}
EXPORT_SYMBOL(scsi_mgmt_handler_unregister);

/**
 * scsi_execute_mgmt_cmd - get command from command cache
 * @sdev: to specify lun
 * @scsi_cmd: scsi command to get
 * @cmd_len: byte length of scsi_cmd
 * @buffer: command result would stored in this memory
 * @buff_len: byte length of buffer
 **/
int scsi_execute_mgmt_cmd(struct scsi_device *sdev, const unsigned char *scsi_cmd,
			int cmd_len, void *buffer, unsigned int buff_len)
{
	int ret = -1;
	struct scsi_mgmt_handler *handler = NULL;
	struct Scsi_Host *shost = NULL;

	if (!scsi_cmd_cache_enabled)
		return -1;

	if (sdev == NULL || scsi_cmd == NULL || buffer == NULL)
		return -1;

	shost = sdev->host;
	if (shost == NULL)
		return -1;

	handler = scsi_mgmt_handler_lookup(shost->host_no);
	if (!handler)
		return -1;

	SCSI_CMD_CACHE_DBG("Found handler of Host%d\n", shost->host_no);

	if (!handler->template->cmd_cache_get)
		goto out;

	SCSI_CMD_CACHE_DBG("[%d:%d:%d:%lld] Getting command 0x%x cache\n",
			shost->host_no,
			sdev->channel,
			sdev->id,
			sdev->lun,
			scsi_cmd[0]);

	ret = handler->template->cmd_cache_get(sdev, scsi_cmd,
			cmd_len, buffer, buff_len);
	if (ret)
		SCSI_CMD_CACHE_ERR("[%d:%d:%d:%lld] Execute command 0x%x failed %d\n",
				shost->host_no,
				sdev->channel,
				sdev->id,
				sdev->lun,
				scsi_cmd[0],
				ret);

out:
	scsi_mgmt_handler_put(handler);
	return ret;
}
EXPORT_SYMBOL(scsi_execute_mgmt_cmd);

/**
 * scsi_cache_mgmt_cmd - set command to command cache
 * @sdev: to specify lun
 * @scsi_cmd: scsi command to set
 * @cmd_len: byte length of scsi_cmd
 * @buffer: command result to set
 * @buff_len: byte length of buffer
 **/
int scsi_cache_mgmt_cmd(struct scsi_device *sdev, const unsigned char *scsi_cmd,
			int cmd_len, void *buffer, unsigned int buff_len)
{
	int ret = -1;
	struct scsi_mgmt_handler *handler = NULL;
	struct Scsi_Host *shost = NULL;

	if (!scsi_cmd_cache_enabled)
		return -1;

	if (sdev == NULL || scsi_cmd == NULL || buffer == NULL)
		return -1;

	shost = sdev->host;
	if (shost == NULL)
		return -1;

	handler = scsi_mgmt_handler_lookup(shost->host_no);
	if (!handler)
		return -1;

	SCSI_CMD_CACHE_DBG("Found handler of Host%d\n", shost->host_no);

	if (!handler->template->cmd_cache_set)
		goto out;

	SCSI_CMD_CACHE_DBG("[%d:%d:%d:%lld] Setting command 0x%x cache\n",
			shost->host_no,
			sdev->channel,
			sdev->id,
			sdev->lun,
			scsi_cmd[0]);

	ret = handler->template->cmd_cache_set(sdev, scsi_cmd,
			cmd_len, buffer, buff_len);
	if (ret)
		SCSI_CMD_CACHE_ERR("[%d:%d:%d:%lld] Setting command 0x%x failed %d\n",
				shost->host_no,
				sdev->channel,
				sdev->id,
				sdev->lun,
				scsi_cmd[0],
				ret);

out:
	scsi_mgmt_handler_put(handler);
	return ret;
}
EXPORT_SYMBOL(scsi_cache_mgmt_cmd);

/**
 * scsi_delete_mgmt_cmd
 * @sdev: specify scsi device
 **/
int scsi_delete_mgmt_cmd(struct scsi_device *sdev)
{
	int ret = -1;
	struct scsi_mgmt_handler *handler = NULL;
	struct Scsi_Host *shost = NULL;

	if (sdev == NULL)
		return -1;

	shost = sdev->host;
	if (shost == NULL)
		return -1;

	handler = scsi_mgmt_handler_lookup(shost->host_no);
	if (!handler)
		return -1;

	SCSI_CMD_CACHE_DBG("Found handler of Host%d\n", shost->host_no);

	if (!handler->template->cmd_cache_remove)
		goto out;

	SCSI_CMD_CACHE_DBG("Remove command cache %d:%d:%d:%lld\n",
			shost->host_no,
			sdev->channel,
			sdev->id,
			sdev->lun);

	ret = handler->template->cmd_cache_remove(sdev);
	if (ret)
		SCSI_CMD_CACHE_ERR("[%d:%d:%d:%lld] Clean cache failed %d\n",
				shost->host_no,
				sdev->channel,
				sdev->id,
				sdev->lun,
				ret);
out:
	scsi_mgmt_handler_put(handler);
	return ret;
}
EXPORT_SYMBOL(scsi_delete_mgmt_cmd);

